/*
 * nlsync
 * Copyright (c) 2010 apechurin 
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using NDesk.Options;

namespace nlsync
{
    /// <summary>
    /// The nlsync tool. See Readme.txt for more information.
    /// Run nlsync.exe --help to get a usage info.
    /// </summary>
    class Program : ISettings
    {
        /// <summary>
        /// The entry point.
        /// </summary>
        static void Main(string[] args)
        {
            Program program = new Program();
            program.Run(args);
        }

        /// <summary>
        /// Default constructor.
        /// </summary>
        Program()
        {
        }

        /// <summary>
        /// Gets the name of the utility.
        /// </summary>
        string ToolName
        {
            get
            {
                return Path.GetFileNameWithoutExtension(
                    Assembly.GetEntryAssembly().Location);
            }
        }

        /// <summary>
        /// Runs the program.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        void Run(string[] args)
        {
            bool working = false;
            
            try
            {
                OptionSet p = CreateOptionSet();

                if (args.Length == 0)
                {
                    ShowHelp(p);
                    return;
                }

                p.Parse(args);
                if (!CheckOptions(p))
                    return;

                working = true;

                Run();
            }
            catch (Exception e)
            {
                Console.Write("{0}: ", ToolName);
                Console.WriteLine(e.Message);

                if (Verbose)
                    Console.WriteLine(e.StackTrace);

                if (!working)
                {
                    Console.WriteLine();
                    Console.WriteLine("Try `{0} --help' for more information.",
                        ToolName);
                }
            }
            finally
            {
                if (mSource != null)
                    mSource.Close();
                if (mTarget != null)
                    mTarget.Close();
            }
        }

        /// <summary>
        /// The main working method.
        /// </summary>
        void Run()
        {
            // for all bundles
            foreach (string bundleName in mSource.BundleNames)
            {
                int added = 0, updated = 0, removed = 0;

                Console.WriteLine("Processing `{0}'...", bundleName);

                IStringBundle sourceBundle = mSource.GetBundle(bundleName);
                IStringBundle targetBundle = mTarget.GetBundle(bundleName);

                // update or add new bundles to the target
                foreach (IStringInfo source in sourceBundle)
                {
                    switch (AddOrUpdateString(source, targetBundle))
                    { 
                        case AddUpdateResult.added:
                            added++;
                            break;
                        case AddUpdateResult.updated:
                            updated++;
                            break;
                    }
                }

                // delete all missed strings
                foreach (IStringInfo target in targetBundle)
                {
                    IStringInfo source = sourceBundle.Find(target.Name);
                    if (source == null)
                    {
                        if (Verbose)
                            Console.WriteLine("! String `{0}': remove", target.Name);
                        
                        targetBundle.Remove(target);
                        removed++;
                    }
                }

                Console.WriteLine("Finished: {0} strings added, " +
                    "{1} strings updated, {2} strings removed.",
                    added, updated, removed);

                sourceBundle.Close();
                targetBundle.Close();
            }
        }

        enum AddUpdateResult
        {
            added,
            updated,
            notChanged
        };

        /// <summary>
        /// Adds a new string or updates it in the target bundle.
        /// </summary>
        /// <param name="source">The string info.</param>
        /// <param name="targetBundle">The target bundle.</param>
        /// <returns>The result.</returns>
        AddUpdateResult AddOrUpdateString(IStringInfo source, 
            IStringBundle targetBundle)
        {
            AddUpdateResult result = AddUpdateResult.notChanged;
            string sourceName = source.Name;
            string sourceValue = source.Value;
            string sourceComment = source.Comment;

            if (Verbose)
                Console.Write("! String `{0}': ", sourceName);

            IStringInfo target = targetBundle.Find(sourceName);
            if (target == null)
            {
                // no need to add if the Value is empty
                if (sourceValue.Length == 0)
                {
                    if (Verbose)
                        Console.WriteLine("ignored");
                    return result;
                }

                target = targetBundle.Add(sourceName);
                result = AddUpdateResult.added;
            }

            if (sourceValue != target.DefaultValue)
            {
                target.DefaultValue = sourceValue;
                if (result != AddUpdateResult.added)
                    result = AddUpdateResult.updated;
            }

            if (targetBundle.IsCommentSupported &&
                sourceComment != target.Comment)
            {
                target.Comment = sourceComment;
                if (result != AddUpdateResult.added)
                    result = AddUpdateResult.updated;
            }

            if (Verbose)
                Console.WriteLine(
                    (result == AddUpdateResult.added) ? "add" :
                    ((result == AddUpdateResult.updated) ? "update" : 
                        "not changed"));
            
            return result;
        }

        /// <summary>
        /// Checks the command line.
        /// </summary>
        bool CheckOptions(OptionSet p)
        {
            if (mDone)
                return false;

            if (mSource == null || mTarget == null)
                throw new Exception("Both SOURCE and TARGET must be specified.");
            
            return true;
        }

        /// <summary>
        /// Creates an option set object which is used to parse a command line.
        /// </summary>
        OptionSet CreateOptionSet()
        { 
            OptionSet p = new OptionSet();
            p
                .Add("h|help|?", "Shows usage information.",
                    delegate(string v) { ShowHelp(p); })
                .Add("xls=", "Specifies SOURCE or TARGET as an Xls file. " +
                     "It's a path to the Xls file.",
                    delegate(string v) { CreateCatalog("xls", v); })
                .Add("resx=", "Specifies SOURCE or TARGET as a set of Resx files. " +
                    "It's a folder where all Resx files are located. " +
                    "It's processed recursively.",
                    delegate(string v) { CreateCatalog("resx", v); })
                .Add("culture=", "The Resx files culture. Optional. If not specified " +
                    "the default culture is assumed.",
                    delegate(string v) { mCulture = v; })
                .Add("resetcolors", "Reset all colors in the following TARGET Xls file before " +
                    "applying any changes. Must be defined before -xls.",
                    delegate(string v) { mResetColors = (v != null); })
                .Add("v|verbose", "Verbose mode.",
                    delegate(string v) { mVerbose = (v != null); });
            return p;
        }

        /// <summary>
        /// Shows usage information.
        /// </summary>
        void ShowHelp(OptionSet p)
        {
            if (mDone)
                return;

            // get assembly version
            Attribute versionAtr = Attribute.GetCustomAttribute(
                Assembly.GetEntryAssembly(), typeof(AssemblyFileVersionAttribute));
            AssemblyFileVersionAttribute version =
                versionAtr as AssemblyFileVersionAttribute;

            Console.WriteLine("{0}, ver. {1}", ToolName, version.Version);
            Console.WriteLine(".NET projects localization helper tool.");
            Console.WriteLine();
            Console.WriteLine("Usage: {0} [OPTIONS] SOURCE TARGET", ToolName);
            Console.WriteLine();
            Console.WriteLine("Options:");
            p.WriteOptionDescriptions(Console.Out);
            Console.WriteLine();

            mDone = true;
        }

        /// <summary>
        /// Creates a source or target catalog of a specified type.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="source">The source string. It's the catalog specific.</param>
        void CreateCatalog(string type, string source)
        {
            if (mSource != null && mTarget != null)
                throw new Exception("Both SOURCE and TARGET are already defined.");
            
            IStringCatalog catalog = StringCatalogFactory.Create(type, source, this);
            if (mSource == null)
                mSource = catalog;
            else
                mTarget = catalog;
        }

        #region ISettings methods

        public bool Verbose
        {
            get
            {
                return mVerbose;
            }
        }
        private bool mVerbose;

        public string Culture
        {
            get
            {
                return mCulture;
            }
        }
        private string mCulture;

        public bool ResetColors
        {
            get
            {
                return mResetColors;
            }
        }
        private bool mResetColors;

        #endregion

        #region Data fields

        /// <summary>
        /// Used to indicate that no more processing required.
        /// </summary>
        bool mDone;

        /// <summary>
        /// The source catalog.
        /// </summary>
        IStringCatalog mSource;

        /// <summary>
        /// The target catalog.
        /// </summary>
        IStringCatalog mTarget;

        #endregion
    }
}
